import Foundation
import Path
import TuistAutomation
import TuistCache
import TuistCore
import TuistDependencies
import TuistGenerator
import TuistServer
import XcodeGraph

#if canImport(TuistCacheEE)
    import TuistCacheEE
#endif

/// The GraphMapperFactorying describes the interface of a factory of graph mappers.
/// Methods in the interface map with workflows exposed to the user.
protocol GraphMapperFactorying {
    /// Returns the graph mapper that should be used for automation tasks such as build and test.
    /// - Returns: A graph mapper.
    func automation(
        config: Tuist,
        testPlan: String?,
        includedTargets: Set<TargetQuery>,
        excludedTargets: Set<TargetQuery>
    ) -> [GraphMapping]

    /// Returns the default graph mapper that should be used from all the commands that require loading and processing the graph.
    /// - Returns: The default mapper.
    func `default`(
        config: Tuist
    ) -> [GraphMapping]
}

public final class GraphMapperFactory: GraphMapperFactorying {
    public init() {}

    public func automation(
        config: Tuist,
        testPlan: String?,
        includedTargets: Set<TargetQuery>,
        excludedTargets: Set<TargetQuery>
    ) -> [GraphMapping] {
        var mappers: [GraphMapping] = []
        mappers.append(
            FocusTargetsGraphMappers(
                testPlan: testPlan,
                includedTargets: includedTargets,
                excludedTargets: excludedTargets
            )
        )
        mappers.append(TreeShakePrunedTargetsGraphMapper())
        mappers.append(contentsOf: self.default(config: config))

        return mappers
    }

    func `default`(
        config: Tuist
    ) -> [GraphMapping] {
        self.default(
            config: config,
            forceWorkspaceSchemes: false,
        )
    }

    public func `default`(
        config: Tuist,
        forceWorkspaceSchemes: Bool
    ) -> [GraphMapping] {
        var mappers: [GraphMapping] = []
        mappers.append(ModuleMapMapper())
        mappers.append(UpdateWorkspaceProjectsGraphMapper())
        mappers.append(ExternalProjectsPlatformNarrowerGraphMapper())
        mappers.append(PruneOrphanExternalTargetsGraphMapper())
        if config.project.generatedProject?.generationOptions.enforceExplicitDependencies == true {
            mappers.append(ExplicitDependencyGraphMapper())
        }

        mappers.append(TreeShakePrunedTargetsGraphMapper())
        mappers.append(StaticXCFrameworkModuleMapGraphMapper())
        mappers.append(
            AutogeneratedWorkspaceSchemeGraphMapper(
                forceWorkspaceSchemes: forceWorkspaceSchemes,
                buildInsightsDisabled: config.project.generatedProject?.generationOptions
                    .buildInsightsDisabled ?? true,
                testInsightsDisabled: config.project.generatedProject?.generationOptions
                    .testInsightsDisabled ?? true
            )
        )
        return mappers
    }

    static func cacheKeepSourceTargets(config: Tuist) -> Bool {
        return config.project.generatedProject?.cacheOptions.keepSourceTargets ?? false
    }
}

#if canImport(TuistCacheEE)
    final class CacheGraphMapperFactory {
        fileprivate let contentHasher: ContentHashing

        init(contentHasher: ContentHashing) {
            self.contentHasher = contentHasher
        }

        func automation(
            config: Tuist,
            ignoreBinaryCache: Bool,
            ignoreSelectiveTesting: Bool,
            testPlan: String?,
            includedTargets: Set<TargetQuery>,
            excludedTargets: Set<TargetQuery>,
            configuration: String?,
            cacheStorage: CacheStoring,
            destination: SimulatorDeviceAndRuntime?
        ) -> [GraphMapping] {
            var mappers: [GraphMapping] = []
            mappers.append(
                AutogeneratedWorkspaceSchemeGraphMapper(
                    forceWorkspaceSchemes: true,
                    buildInsightsDisabled: config.project.generatedProject?.generationOptions
                        .buildInsightsDisabled ?? true,
                    testInsightsDisabled: config.project.generatedProject?.generationOptions
                        .testInsightsDisabled ?? true
                )
            )
            mappers.append(ModuleMapMapper())

            // Apply platform narrowing early for consistent hashing
            mappers.append(ExternalProjectsPlatformNarrowerGraphMapper())
            mappers.append(PruneOrphanExternalTargetsGraphMapper())

            mappers.append(
                TestsCacheGraphMapper(
                    testPlan: testPlan,
                    includedTargets: includedTargets,
                    excludedTargets: excludedTargets,
                    cacheStorage: cacheStorage,
                    ignoreSelectiveTesting: ignoreSelectiveTesting,
                    destination: destination
                )
            )

            mappers.append(TreeShakePrunedTargetsGraphMapper())

            mappers.append(
                FocusTargetsGraphMappers(
                    testPlan: testPlan,
                    includedTargets: includedTargets,
                    excludedTargets: excludedTargets
                )
            )

            if !ignoreBinaryCache {
                let focusTargetsGraphMapper = TargetsToCacheBinariesGraphMapper(
                    config: config,
                    decider: CacheProfileTargetReplacementDecider(
                        profile: .allPossible,
                        exceptions: includedTargets
                    ),
                    configuration: configuration,
                    cacheStorage: cacheStorage
                )
                mappers.append(focusTargetsGraphMapper)
                mappers.append(TreeShakePrunedTargetsGraphMapper())
            }

            mappers.append(UpdateWorkspaceProjectsGraphMapper())

            if config.project.generatedProject?.generationOptions.enforceExplicitDependencies
                == true
            {
                mappers.append(ExplicitDependencyGraphMapper())
            }

            mappers.append(StaticXCFrameworkModuleMapGraphMapper())

            return mappers
        }

        func build(
            config: Tuist,
            ignoreBinaryCache: Bool,
            configuration: String?,
            cacheStorage: CacheStoring
        ) -> [GraphMapping] {
            var mappers: [GraphMapping] = []
            mappers.append(ModuleMapMapper())

            // Apply platform narrowing early for consistent hashing
            mappers.append(ExternalProjectsPlatformNarrowerGraphMapper())
            mappers.append(PruneOrphanExternalTargetsGraphMapper())
            mappers.append(TreeShakePrunedTargetsGraphMapper())

            if !ignoreBinaryCache {
                let focusTargetsGraphMapper = TargetsToCacheBinariesGraphMapper(
                    config: config,
                    decider: CacheProfileTargetReplacementDecider(
                        profile: .onlyExternal,
                        exceptions: []
                    ),
                    configuration: configuration,
                    cacheStorage: cacheStorage
                )
                mappers.append(focusTargetsGraphMapper)
                mappers.append(TreeShakePrunedTargetsGraphMapper())
            }

            mappers.append(UpdateWorkspaceProjectsGraphMapper())

            if config.project.generatedProject?.generationOptions.enforceExplicitDependencies
                == true
            {
                mappers.append(ExplicitDependencyGraphMapper())
            }

            mappers.append(StaticXCFrameworkModuleMapGraphMapper())
            mappers.append(
                AutogeneratedWorkspaceSchemeGraphMapper(
                    forceWorkspaceSchemes: false,
                    buildInsightsDisabled: config.project.generatedProject?.generationOptions
                        .buildInsightsDisabled ?? true,
                    testInsightsDisabled: config.project.generatedProject?.generationOptions
                        .testInsightsDisabled ?? true
                )
            )

            return mappers
        }

        func generation(
            config: Tuist,
            cacheProfile: CacheProfile,
            cacheSources: Set<TargetQuery>,
            configuration: String?,
            cacheStorage: CacheStoring
        ) -> [GraphMapping] {
            var mappers: [GraphMapping] = []
            mappers.append(ModuleMapMapper())

            // Narrow the external project platforms
            mappers.append(ExternalProjectsPlatformNarrowerGraphMapper())
            mappers.append(PruneOrphanExternalTargetsGraphMapper())
            mappers.append(TreeShakePrunedTargetsGraphMapper())

            if !GraphMapperFactory.cacheKeepSourceTargets(config: config) {
                mappers.append(FocusTargetsGraphMappers(includedTargets: cacheSources))
                mappers.append(TreeShakePrunedTargetsGraphMapper())
            }

            if cacheProfile != .none {
                let focusTargetsGraphMapper = TargetsToCacheBinariesGraphMapper(
                    config: config,
                    decider: CacheProfileTargetReplacementDecider(
                        profile: cacheSources.isEmpty ? cacheProfile : .allPossible,
                        exceptions: cacheSources
                    ),
                    configuration: configuration,
                    cacheStorage: cacheStorage
                )
                mappers.append(focusTargetsGraphMapper)
                mappers.append(TreeShakePrunedTargetsGraphMapper())
            }

            mappers.append(UpdateWorkspaceProjectsGraphMapper())

            if config.project.generatedProject?.generationOptions.enforceExplicitDependencies
                == true
            {
                mappers.append(ExplicitDependencyGraphMapper())
            }

            mappers.append(StaticXCFrameworkModuleMapGraphMapper())

            mappers.append(
                AutogenerateTuistGenerateSchemeMapper(
                    includeGenerateScheme: config.project.generatedProject?
                        .generationOptions.includeGenerateScheme ?? false
                )
            )
            mappers.append(
                AutogeneratedWorkspaceSchemeGraphMapper(
                    forceWorkspaceSchemes: true,
                    buildInsightsDisabled: config.project.generatedProject?.generationOptions
                        .buildInsightsDisabled ?? true,
                    testInsightsDisabled: config.project.generatedProject?.generationOptions
                        .testInsightsDisabled ?? true
                )
            )

            return mappers
        }

        func binaryCacheWarming(
            config: Tuist,
            cacheSources: Set<TargetQuery>,
            configuration: String,
            cacheStorage: CacheStoring
        ) -> [GraphMapping] {
            var mappers: [GraphMapping] = []
            mappers.append(ModuleMapMapper())
            mappers.append(ExternalProjectsPlatformNarrowerGraphMapper())
            mappers.append(FocusTargetsGraphMappers(includedTargets: cacheSources))
            mappers.append(TreeShakePrunedTargetsGraphMapper())

            let focusTargetsGraphMapper = TargetsToCacheBinariesGraphMapper(
                config: config,
                decider: CacheProfileTargetReplacementDecider(
                    profile: .allPossible,
                    exceptions: cacheSources
                ),
                configuration: configuration,
                cacheStorage: cacheStorage
            )
            mappers.append(focusTargetsGraphMapper)
            mappers.append(TreeShakePrunedTargetsGraphMapper())

            mappers.append(UpdateWorkspaceProjectsGraphMapper())
            mappers.append(PruneOrphanExternalTargetsGraphMapper())

            if config.project.generatedProject?.generationOptions.enforceExplicitDependencies
                == true
            {
                mappers.append(ExplicitDependencyGraphMapper())
            }

            mappers.append(StaticXCFrameworkModuleMapGraphMapper())
            mappers.append(
                AutogeneratedWorkspaceSchemeGraphMapper(
                    forceWorkspaceSchemes: false,
                    buildInsightsDisabled: config.project.generatedProject?.generationOptions
                        .buildInsightsDisabled ?? true,
                    testInsightsDisabled: config.project.generatedProject?.generationOptions
                        .testInsightsDisabled ?? true
                )
            )

            return mappers
        }

        func binaryCacheWarmingPreload(targetsToBinaryCache: Set<TargetQuery>, config: Tuist)
            -> [GraphMapping]
        {
            var mappers: [GraphMapping] = []
            mappers.append(ModuleMapMapper())
            mappers.append(ExternalProjectsPlatformNarrowerGraphMapper())
            mappers.append(FocusTargetsGraphMappers(includedTargets: targetsToBinaryCache))
            mappers.append(TreeShakePrunedTargetsGraphMapper())

            mappers.append(UpdateWorkspaceProjectsGraphMapper())
            mappers.append(PruneOrphanExternalTargetsGraphMapper())

            if config.project.generatedProject?.generationOptions.enforceExplicitDependencies
                == true
            {
                mappers.append(ExplicitDependencyGraphMapper())
            }

            mappers.append(
                AutogeneratedWorkspaceSchemeGraphMapper(
                    forceWorkspaceSchemes: false,
                    buildInsightsDisabled: config.project.generatedProject?.generationOptions
                        .buildInsightsDisabled ?? true,
                    testInsightsDisabled: config.project.generatedProject?.generationOptions
                        .testInsightsDisabled ?? true
                )
            )

            return mappers
        }

        func `default`(config: Tuist) -> [GraphMapping] {
            self.default(config: config, forceWorkspaceSchemes: false)
        }

        func `default`(config: Tuist, forceWorkspaceSchemes: Bool) -> [GraphMapping] {
            var mappers: [GraphMapping] = []
            mappers.append(ModuleMapMapper())
            mappers.append(UpdateWorkspaceProjectsGraphMapper())
            mappers.append(ExternalProjectsPlatformNarrowerGraphMapper())
            mappers.append(PruneOrphanExternalTargetsGraphMapper())
            if config.project.generatedProject?.generationOptions.enforceExplicitDependencies
                == true
            {
                mappers.append(ExplicitDependencyGraphMapper())
            }
            mappers.append(TreeShakePrunedTargetsGraphMapper())
            mappers.append(StaticXCFrameworkModuleMapGraphMapper())
            mappers.append(
                AutogeneratedWorkspaceSchemeGraphMapper(
                    forceWorkspaceSchemes: forceWorkspaceSchemes,
                    buildInsightsDisabled: config.project.generatedProject?.generationOptions
                        .buildInsightsDisabled ?? true,
                    testInsightsDisabled: config.project.generatedProject?.generationOptions
                        .testInsightsDisabled ?? true
                )
            )
            return mappers
        }

        /// Default mappers which are run as last.
        /// Mappers that impact the hash of individual modules shouldn't be here and should be run earlier in the process.
        fileprivate func defaultPostMappers(config: Tuist) -> [GraphMapping] {
            self.default(config: config)
                .filter {
                    !($0 is PruneOrphanExternalTargetsGraphMapper)
                        && !($0 is ExternalProjectsPlatformNarrowerGraphMapper)
                        && !($0 is ModuleMapMapper)
                        && !($0 is AutogeneratedWorkspaceSchemeGraphMapper)
                }
        }
    }
#endif
